Skip to content

Expand Visualizer Tests and Patch Visualizer Bugs#5103

Open
matthewtrepte wants to merge 44 commits intoisaac-sim:developfrom
matthewtrepte:mtrepte/update_viz_2
Open

Expand Visualizer Tests and Patch Visualizer Bugs#5103
matthewtrepte wants to merge 44 commits intoisaac-sim:developfrom
matthewtrepte:mtrepte/update_viz_2

Conversation

@matthewtrepte
Copy link
Copy Markdown
Contributor

@matthewtrepte matthewtrepte commented Mar 24, 2026

Description

Tests

Newton Visualizer

  • Fix "Pause/Resume Rendering" button
  • Change "Pause/Resume Training" to "Pause/Resume Simulation"

Kit Visualizer

  • Fix recording in headless mode
  • Resolve overlapping camera cfg behavior in ViewerCfg and KitVisualizerCfg (will likely need another PR + design to streamline the cfgs)

Rerun Visualizer

  • Fix issue when launching rerun + newton physics without KitVisualizer

RTX Renderer

  • Fix stale image issue after resets which require Kit Visualizer's continuous app updates to avoid
  • Fix strange wrist camera orientation offset issue

Else

  • Rename camera_position/camera_target_position fields to eye/lookat across the board

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (existing functionality will not work without user modification)
  • Documentation update

Screenshots

Please attach before and after screenshots of the change if applicable.

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the changelog and the corresponding version in the extension's config/extension.toml file
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

@github-actions github-actions bot added the isaac-lab Related to Isaac Lab team label Mar 24, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR expands visualizer test coverage with Cartpole integration tests (non-black frame and motion checks), adds regression tests from #5056, and fixes several bugs: the Newton viewer "Pause/Resume" button labels, stale RTX images after resets, Rerun launching without KitVisualizer, Kit headless recording, and overlapping ViewerCfg/KitVisualizerCfg camera config. It also renames camera_position/camera_target_position fields to eye/lookat across the board and adds a deprecation warning to ViewerCfg.

  • The PR checklist shows Changelog and extension.toml version bump are unchecked. Per AGENTS.md, every change targeting source/ must ship with new version headings in the relevant CHANGELOG.rst files and matching version bumps in config/extension.toml.
  • The eye/lookat rename is a breaking API change; AGENTS.md requires deprecating old names before removal.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/UX concerns with no correctness or data-integrity issues.

No P0 or P1 issues found. The is_training_paused()/playSimulations interaction was already flagged in a prior thread. Remaining items — missing render_product cleanup, browser-open skipped for new Rerun server, missing SimulationContext guard at test start, and missing CHANGELOG/version bump — are all P2 quality concerns that do not affect runtime correctness.

source/isaaclab_visualizers/test/test_visualizer_cartpole_integration.py (test isolation and resource cleanup), source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer.py (is_training_paused flag overlap)

Important Files Changed

Filename Overview
source/isaaclab_visualizers/test/test_visualizer_cartpole_integration.py New integration test suite for all four visualizer backends × two physics backends; well-structured with helpers and parametrize, but missing SimulationContext.clear_instance() at test start and render_product cleanup in the Kit test.
source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer.py Rewrites step() to guard playSimulations around app.update(); is_training_paused() now reads the same setting, creating a brief window where it returns True during every step() call.
source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer_utils.py Adds ensure_isaac_rtx_render_update() with per-step dedup key to fix stale RTX images after resets; correctly skips when KitVisualizer is already pumping app.update().
source/isaaclab/isaaclab/envs/common.py Adds DeprecationWarning to ViewerCfg.post_init whenever any field deviates from its default, directing users to configure viewport via KitVisualizerCfg/NewtonVisualizerCfg instead.
source/isaaclab/isaaclab/visualizers/visualizer_cfg.py Renames camera_position/camera_target_position to eye/lookat; no backward-compat shim for the renamed fields.
source/isaaclab_visualizers/isaaclab_visualizers/rerun/rerun_visualizer.py Fixes rerun initialization without KitVisualizer; open_browser only fires when connecting to an existing server to avoid premature browser open before the server is ready.
source/isaaclab/test/sim/test_simulation_context_visualizers.py New unit tests for SimulationContext visualizer orchestration with good fake/monkeypatch coverage for training-pause, rendering-pause, app-pumping dedup, and CLI handling.
source/isaaclab_physx/isaaclab_physx/renderers/kit_viewport_utils.py New helper set_kit_renderer_camera_view() extracted from SimulationContext to keep the core backend-agnostic.

Sequence Diagram

sequenceDiagram
    participant Env as DirectRLEnv
    participant SC as SimulationContext
    participant PM as PhysicsManager
    participant KitViz as KitVisualizer
    participant RTX as ensure_rtx_render_update()
    participant App as omni.kit.app

    Env->>SC: step(render=True)
    SC->>PM: step()
    SC->>SC: render()
    SC->>PM: pre_render()
    SC->>SC: update_visualizers(dt)
    SC->>KitViz: is_training_paused()?
    KitViz-->>SC: reads /app/player/playSimulations
    SC->>KitViz: step(dt)
    KitViz->>App: set playSimulations=False
    KitViz->>App: app.update()
    KitViz->>App: set playSimulations=True
    SC->>PM: after_visualizers_render()
    Note over PM,RTX: PhysxManager calls ensure_rtx_render_update()
    RTX->>SC: check any viz.pumps_app_update()?
    alt KitVisualizer active
        RTX-->>PM: skip - KitViz already pumped
    else No KitVisualizer
        RTX->>App: set playSimulations=False
        RTX->>App: app.update()
        RTX->>App: set playSimulations=True
    end
Loading

Reviews (2): Last reviewed commit: "Merge branch 'develop' into mtrepte/upda..." | Re-trigger Greptile

Comment thread source/isaaclab/setup.py Outdated
"torch>=2.10",
"onnx>=1.18.0", # 1.16.2 throws access violation on Windows
"prettytable==3.3.0",
"munch",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 munch dependency has no version constraint

All other dependencies in this file are pinned or have minimum version bounds (e.g. torch>=2.10, prettytable==3.3.0). An unpinned munch can silently pull in a breaking release. Consider adding a lower bound that has been verified to work:

Suggested change
"munch",
"munch>=4.0.0",

Comment on lines 105 to 113
try:
import omni.kit.app

from isaaclab.app.settings_manager import get_settings_manager

app = omni.kit.app.get_app()
if app is not None and app.is_running():
# Keep app pumping for viewport/UI updates only.
# Simulation stepping is owned by SimulationContext.
settings = get_settings_manager()
settings.set_bool("/app/player/playSimulations", False)
# Pump Kit/UI events only; SimulationContext owns physics stepping.
app.update()
settings.set_bool("/app/player/playSimulations", True)
except (ImportError, AttributeError) as exc:
logger.debug("[KitVisualizer] App update skipped: %s", exc)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Physics double-stepping risk without the old playSimulations guard

The removed code blocked Kit from advancing physics during app.update() by temporarily setting /app/player/playSimulations to False. The guard was clearly broken — step() toggled the flag back to True immediately, so the old is_training_paused() could never observe False — but removing it entirely means Kit is now free to tick physics during app.update() when the timeline is in "playing" state, which could cause double-stepping alongside SimulationContext.

In practice Isaac Sim's app.update() should not independently step physics (physics ownership is expected to live in SimulationContext), and the new is_training_paused() correctly checks the timeline transport state rather than the now-gone setting. However, if a regression appears where physics advances twice per simulation step, this is the first place to investigate.

It would be worth adding a short comment here explaining why the guard was removed (it was effectively a no-op), so future readers don't re-introduce it.

Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot — PR #5103

Summary

This PR addresses two visualizer bugs and expands test coverage with non-black-frame integration tests. The changes span 5 files:

  1. simulation_context.py — Adds viz.step(0.0) when rendering is paused, keeping the visualizer event loop responsive so UI controls (e.g. "Resume Rendering") remain interactive.
  2. setup.py — Adds munch as a dependency for the Newton visualizer.
  3. test_simulation_context_visualizers.py — Adds a dedicated unit test for the rendering-paused fix and updates an existing test assertion.
  4. kit_visualizer.py — Rewrites step() to use carb.settings directly (replacing get_settings_manager), and conditionally skips app.update() when playSimulations is already falsy. Rewrites is_training_paused() to use omni.timeline instead of the settings flag.
  5. test_visualizer_smoke_logs.py — Adds ~174 lines of integration test infrastructure and two new parametrized tests (test_kit_visualizer_non_black_viewport_frame, test_newton_visualizer_non_black_viewer_frame) that load a Cartpole camera scene and assert non-black rendered output.

Design Assessment

Rendering-paused fix (simulation_context.py): ✅ Sound approach. Calling viz.step(0.0) with zero dt while rendering is paused keeps event loops responsive without advancing simulation time. The fix targets the root cause — previously the continue statement completely skipped the visualizer, leaving its UI unresponsive. The zero-dt convention is already established for the training-pause loop on the adjacent line.

Kit visualizer step() rewrite: ✅ Good improvement. Replacing get_settings_manager() with direct carb.settings access removes an unnecessary abstraction layer. The conditional should_restore logic avoids the previous bug where app.update() was unconditionally called even when simulations weren't playing, which caused the Play/Pause button to erroneously unpause the physics.

Kit visualizer is_training_paused() rewrite: ✅ Correct. Using omni.timeline transport state is semantically accurate — the old implementation checked the /app/player/playSimulations settings flag which could be transiently toggled by step() itself, creating false positives for training pause detection. Timeline transport state is the authoritative source.

Non-black-frame tests: ⚠️ Reasonable intent but CI shows these new tests are fragile in shared GPU CI environments. The CUDA OOM and illegal memory access errors causing test failures are likely caused by test process isolation issues (GPU state leaking between test cases), not by logic bugs in this PR.


Architecture Impact

  • Low risk. The simulation_context.py change is a one-line addition inside an existing code path.
  • The kit_visualizer.py changes replace one internal API (get_settings_manager) with another (carb.settings), reducing coupling. The timeline-based pause detection is more robust.
  • Adding munch to setup.py is a new runtime dependency — this is appropriate if Newton visualizer requires it, but the version should ideally be pinned (see Finding #1).

Implementation Verdict

Approve with findings. The bug fixes are correct and well-targeted. The test infrastructure is solid. However, the new integration tests have CI reliability issues that should be addressed before merge.


Test Coverage

Area Coverage
Rendering-paused viz.step(0.0) ✅ Dedicated unit test + updated existing test assertion
Kit step() conditional update ⚠️ Covered by integration smoke test, but no dedicated unit test mocking carb.settings
Kit is_training_paused() timeline ⚠️ No unit test for the new timeline-based implementation
Non-black frame detection helpers ✅ Exercised by integration tests
Non-black viewport tests 🔴 3/3 new integration test parametrizations failing in CI

CI Status

Job Status Notes
isaaclab_visualizers 🔴 FAIL 3 new tests fail: test_kit_visualizer_non_black_viewport_frame[newton] (CUDA OOM during render product allocation), test_newton_visualizer_non_black_viewer_frame[physx] and [newton] (CUDA illegal memory access during SimulationContext init — GPU state corrupted by prior test)
isaaclab (core) [1-3/3] 🔴 FAIL Failures in test_non_headless_launch, test_first_frame_textured_rendering, test_ray_caster_cameraunrelated to this PR
isaaclab_physx 🔴 FAIL Likely pre-existing, unrelated
isaaclab_tasks [3/3] 🔴 FAIL Likely pre-existing, unrelated
environments_training 🔴 FAIL Likely pre-existing, unrelated
pre-commit ✅ PASS
license-check ✅ PASS

Findings

Finding 1 — munch dependency is unpinned

File: source/isaaclab/setup.py, line 25
Severity: Low
Issue: munch is added without a version pin. Every other dependency in this section has an explicit version or minimum version constraint. An unpinned munch could cause unexpected breakage if a future major version introduces breaking API changes.
Suggestion: Pin to a minimum version, e.g. "munch>=4.0.0" or "munch==4.0.0".

Finding 2 — Kit step() skips app.update() when playSimulations is already falsy

File: source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer.py, lines 108-119
Severity: Medium
Issue: The new logic only calls app.update() inside if should_restore, meaning when playSimulations is already False, app.update() is never called at all. The old code unconditionally called app.update() (after disabling playSimulations). This behavioral change means the Kit viewport will not pump UI events when simulation is already paused, which could prevent viewport refresh in paused-then-rendering scenarios.

# Current:
should_restore = bool(previous_play_simulations)
if should_restore:
    settings.set_bool(play_simulations_path, False)
    app.update()
if should_restore:
    settings.set_bool(play_simulations_path, True)

# Consider instead:
should_restore = bool(previous_play_simulations)
if should_restore:
    settings.set_bool(play_simulations_path, False)
app.update()  # Always pump UI events
if should_restore:
    settings.set_bool(play_simulations_path, True)

Alternatively, if the intent is specifically to skip app.update() when paused, document this behavior with a comment explaining why.

Finding 3 — New integration tests are not resilient to GPU state corruption from prior tests

File: source/isaaclab_visualizers/test/test_visualizer_smoke_logs.py
Severity: Medium
Issue: The CI failures show that test_newton_visualizer_non_black_viewer_frame[physx] and [newton] fail with torch.AcceleratorError: CUDA error: an illegal memory access during SimulationContext.__init__ — this is GPU state corruption carried over from the preceding test_kit_visualizer_non_black_viewport_frame[newton] failure (CUDA OOM in render product allocation). Since all tests run in a single process (required by Kit app lifetime), a CUDA error in one test poisons all subsequent GPU operations.
Suggestion: Consider adding pytest.mark.order to run the non-black frame tests last, or adding a CUDA device reset/sync check at the start of each test. Alternatively, mark these as @pytest.mark.xfail(reason="Requires dedicated GPU resources") until the CI environment is stabilized, so they don't block the PR pipeline.

Finding 4 — PR description mentions a known TODO that is not addressed

Severity: Info
Issue: The PR body states: "TODO: fix Unpause behavior, currently body transforms remain stale after Unpausing... recent regression". This indicates a known remaining bug in the Kit visualizer's pause/unpause lifecycle. This is fine as a known limitation, but consider filing a tracking issue so it doesn't get lost.


Review by 🤖 Isaac Lab Review Bot — 4a42c18b

@github-actions github-actions bot added the documentation Improvements or additions to documentation label Apr 1, 2026
Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot — Follow-up

New commits (4a42c18→053adbd4): renamed camera_position/camera_targeteye/lookat across the codebase, simplified Kit step(), and merged develop.

One new issue found in the rename.

Comment thread source/isaaclab/isaaclab/envs/common.py
@matthewtrepte matthewtrepte requested a review from xyao-nv as a code owner April 3, 2026 00:16
@github-actions github-actions bot added the isaac-mimic Related to Isaac Mimic team label Apr 3, 2026
Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot — PR #5103

Summary

This PR addresses multiple visualizer bugs (Newton pause button, Kit headless recording, rerun+newton launch, RTX stale images, wrist camera orientation), renames camera_position/camera_target to eye/lookat across the board, deprecates ViewerCfg overrides in favor of VisualizerCfg subclasses, and replaces the lightweight smoke tests with comprehensive cartpole-based integration tests that verify non-black frames and motion detection. The scope is significant — 24 files touching core simulation, env initialization, physics hooks, visualizer implementations, installation tooling, and test infrastructure.

Design Assessment

Mostly sound, with one architectural concern flagged by the maintainer.

The overall direction — dedicated visualizer cameras, deprecating ViewerCfg, decoupling Kit-specific logic from SimulationContext — is correct and moves toward cleaner separation. The after_visualizers_render() hook in PhysicsManager with the Kit-specific override in PhysxManager is a reasonable pattern to keep SimulationContext backend-agnostic.

However, has_active_visualizers() and can_render_rgb_array() in SimulationContext (simulation_context.py) directly query Isaac Sim settings paths (/isaaclab/visualizer/types). As maintainer @kellyguo11 noted, this introduces backend-specific coupling into what should be a backend-agnostic class. Consider moving these helpers to the PhysicsManager interface or making the setting path discovery pluggable.

Findings

🔴 Critical: _pending_camera_view not set in unit test helpers — CI failuresimulation_context.py:603 / test_simulation_context_visualizers.py

The _pending_camera_view attribute is initialized in SimulationContext.__init__(), but several unit tests create context objects via object.__new__(SimulationContext) (bypassing __init__). The _make_context_with_settings helper was updated to set _pending_camera_view = None and _render_generation = 0, but the simpler _make_context() helper was not. This causes the AttributeError that @kellyguo11 reported in CI:

E   AttributeError: 'SimulationContext' object has no attribute '_pending_camera_view'

The _make_context() function (test line ~130) also needs _pending_camera_view = None and _render_generation = 0 set on the context object, just as _make_context_with_settings() does.

🔴 Critical: _resolve_cfg_camera_pose uses bare getattr without fallbackbase_visualizer.py:196-198

eye = tuple(float(v) for v in getattr(self.cfg, "eye"))
lookat = tuple(float(v) for v in getattr(self.cfg, "lookat"))

getattr(obj, name) without a default raises AttributeError if the attribute is missing — identical to self.cfg.eye. The getattr wrapping provides no safety benefit here and is misleading. If the intent is to support subclasses with different field names, add a default and handle the missing case. If eye/lookat are always present (they are, on VisualizerCfg), use direct attribute access for clarity:

eye = tuple(float(v) for v in self.cfg.eye)
lookat = tuple(float(v) for v in self.cfg.lookat)

The del visualizer_name is also a code smell — if the parameter is unused, either drop it from the signature or document why it exists for subclass overrides.

🟡 Warning: _viewer_cfg_value_matches_default fragile for nested configclass fieldsenvs/common.py:22-29

The deprecation check in ViewerCfg.__post_init__ compares each field to its declared default. This works for simple types but will silently miss nested @configclass fields whose defaults are factory-constructed — f.default would be MISSING and f.default_factory is a dataclasses concept that @configclass may not always set identically. For now all ViewerCfg fields are simple scalars/tuples so this is safe, but the approach should be noted as fragile if fields are added later.

🟡 Warning: pump_kit_app_for_headless_video_render_if_needed catches all exceptions silentlyisaac_rtx_renderer_utils.py:138-139

    except Exception as exc:
        logger.debug("[isaac_rtx] Skipping Kit app-loop pump in render(): %s", exc)

A bare except Exception with only debug-level logging around ensure_isaac_rtx_render_update() means that genuine rendering failures (e.g., GPU resource exhaustion, CUDA errors) will be silently swallowed. At minimum, this should log at warning level so operators can diagnose headless video recording failures. The debug level is appropriate for expected non-Kit environments, but unexpected errors deserve more visibility.

🟡 Warning: Breaking change in camera config field names not documented in CHANGELOGvisualizer_cfg.py

Renaming camera_positioneye, camera_targetcamera_target, camera_sourcecam_source, camera_usd_pathcam_prim_path is a breaking change for any user with custom VisualizerCfg in their training scripts. The PR description mentions the rename but per Isaac Lab contributing guidelines, breaking changes must be documented in CHANGELOG.rst. No CHANGELOG update is included.

Additionally, the old → new error path changed from a warning + fallback (graceful) to a RuntimeError (crash) when cam_source='prim_path' fails to resolve. This is a stricter behavior change that should be called out:

# Before: warning + fallback to cfg camera
logger.warning("[NewtonVisualizer] camera_usd_path '%s' not found; using configured camera.", ...)
# After: hard crash
raise RuntimeError("[NewtonVisualizer] cam_source='prim_path' requires a resolvable camera prim path...")

While fail-fast is generally better for misconfigurations, this is a behavior change for existing users whose USD camera paths resolve at runtime (e.g., deferred scene loading).

🟡 Warning: KitVisualizerCfg.create_viewport default changed from False to Truekit_visualizer_cfg.py

This is a silent behavior change: all KitVisualizerCfg() users who relied on the default create_viewport=False will now get a new viewport created automatically. Combined with the new validation that viewport_name must be non-empty when create_viewport=True, this could surprise existing users.

🔵 Suggestion: Prebundle repoint search across symlinks could be slowcli/commands/install.py:358-374

The new prebundle discovery resolves symlinks and searches up to 6+ root directories with rglob("pip_prebundle"). On systems with large Omniverse caches (e.g., ~/.local/share/ov/data/exts with hundreds of extensions), this could be noticeably slow. Consider caching the result or adding a debug-level log of how many dirs were scanned and how long it took, to aid diagnosis of slow isaaclab install runs.

🔵 Suggestion: Test module-level simulation_app launch prevents test isolationtest_visualizer_cartpole_integration.py:25

simulation_app = AppLauncher(headless=True, enable_cameras=True).app

Launching the Kit app at module import time means all tests in this file share a single app instance. This is standard for Isaac Lab tests, but worth noting that test ordering could matter — a test that corrupts app state (e.g., a failed viewport creation) could cascade failures to subsequent tests.

Test Coverage

  • Bug fixes: Partial regression coverage. The Newton pause button fix has a unit test (test_update_visualizers_skips_zero_dt_for_paused_app_pumping_visualizer). The RTX stale image fix is exercised via the Kit viewport motion test. However, the headless recording fix and rerun+newton launch fix don't have dedicated regression tests — they're implicitly covered by the integration tests working, but a reintroduction of the bug might not be caught.
  • New integration tests are comprehensive. The cartpole-based tests verify non-black frames, frame-to-frame motion detection (early vs late captures), and log hygiene across 4 visualizer × 2 backend combinations. The motion threshold approach (channel diff ≥ 50, ≥ 100 differing pixels) is well-calibrated for catching frozen/stale rendering.
  • Deleted smoke tests replaced adequately. The old test_visualizer_smoke_logs.py tested initialization + stepping with a minimal env. The new tests use a real cartpole scene which is strictly more rigorous.
  • Gap: No test for ViewerCfg deprecation warning. The __post_init__ deprecation path is only tested indirectly via test_configclass.py wrapping an existing test in pytest.warns. A direct test that ViewerCfg(eye=(1,1,1)) emits the warning while ViewerCfg() does not would be valuable.

CI Status

All checks passing ✅ (pre-commit, license-check, build wheel, docs, installation tests). Note: the CI failure @kellyguo11 reported (AttributeError: '_pending_camera_view') may have been from a prior commit — the current SHA adds the attribute in _make_context_with_settings but not in _make_context, so the issue likely persists for the test_non_explicit_create_failure_silently_logged test path.

Verdict

Significant concerns

The architecture and direction are right, but there are blocking issues: a CI-failing AttributeError from incomplete test helper updates, a breaking config rename without CHANGELOG documentation, and maintainer questions about SimulationContext backend coupling that need resolution. The new integration tests are high quality and a clear improvement. Recommend addressing the CI failure and maintainer feedback before merging.

Comment thread source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py Outdated
Comment thread source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py Outdated
#
# SPDX-License-Identifier: BSD-3-Clause

"""Kit / Omniverse viewport helpers (Isaac Sim specific).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this belong in the renderers? or more for visualizers?

Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot — PR #5103

Summary

This PR delivers a substantial overhaul of the visualizer subsystem:

  1. Camera config rename: camera_position/camera_targeteye/lookat, camera_source/camera_usd_pathcam_source/cam_prim_path — consistent API across all visualizers.
  2. ViewerCfg deprecation: __post_init__ warning when non-default fields are overridden, migrating toward KitVisualizerCfg/NewtonVisualizerCfg as the sole camera config surface.
  3. Bug fixes: Pause-rendering button in Newton, headless video recording in Kit, stale RTX image after resets, USD stage traversal safety, wrist camera orientation offset.
  4. New render lifecycle hooks: after_visualizers_render() on PhysicsManager, render_generation counter, _pending_camera_view replay.
  5. Kit visualizer: Dedicated camera prim (enable_visualizer_cam, visualizer_camera_prim_path, focal_length), viewport decoupling from sensor cameras.
  6. Prebundle repointing: Expanded to Omniverse cache roots and new packages (websockets, viser, imgui_bundle).
  7. Tests: Replaced smoke test with comprehensive cartpole integration tests including non-black frame and motion assertions.

Overall this is a well-structured PR with clear separation of concerns. Inline comments below cover specific issues found.

Verdict

🟡 Good with minor issues — A few robustness and correctness items to address before merge (see inline comments).

) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
"""Resolve camera pose from cfg eye/lookat fields."""
del visualizer_name
eye = tuple(float(v) for v in getattr(self.cfg, "eye"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: Using bare getattr(self.cfg, "eye") without a default will raise AttributeError if a subclass cfg doesn't define eye/lookat (same as direct attribute access). Since this is a base-class method meant to be inherited by all visualizers, consider defensive access with a fallback or an explicit check:

Suggested change
eye = tuple(float(v) for v in getattr(self.cfg, "eye"))
eye = tuple(float(v) for v in getattr(self.cfg, "eye", (7.5, 7.5, 7.5)))
lookat = tuple(float(v) for v in getattr(self.cfg, "lookat", (0.0, 0.0, 0.0)))

Alternatively, use self.cfg.eye directly (clearer intent) if all VisualizerCfg subclasses are guaranteed to have these fields — the getattr without a default gives no benefit over direct attribute access.

self, visualizer_name: str
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
"""Resolve camera pose from cfg eye/lookat fields."""
del visualizer_name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: The visualizer_name parameter is accepted then immediately deleted. If it's reserved for future subclass overrides, a leading underscore or # noqa would better signal that intent. As-is, callers like NewtonVisualizer and RerunVisualizer pass a name string that is silently discarded, which could confuse future maintainers.

Consider either:

  • Removing the parameter if no subclass uses it, or
  • Using _visualizer_name with a comment explaining the future intent.

# Replay any camera pose requested before visualizers were initialized.
pending = getattr(self, "_pending_camera_view", None)
if pending is not None:
eye, target = pending
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: _pending_camera_view is never cleared after replay. If initialize_visualizers() is called multiple times (e.g. on a re-init), the stale pending pose will be re-applied to the new set of visualizers. Consider clearing it after replay:

Suggested change
eye, target = pending
pending = getattr(self, "_pending_camera_view", None)
if pending is not None:
eye, target = pending
for viz in self._visualizers:
viz.set_camera_view(eye, target)
self._pending_camera_view = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to enforce that initialize_visualizers only gets called once?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. There's already a guard at the top of the method (if self._visualizers: return) that makes repeated calls a no-op, which handles the common case well. That said, the guard is based on the list being non-empty — if initialization ran but all visualizers failed (leaving self._visualizers as []), a second call would re-run everything.

For robustness, an explicit _visualizers_initialized flag (checked/set at the top) would be the cleanest way to enforce single initialization regardless of outcome. But that's a broader design choice — the current early-return + clearing _pending_camera_view (which the author has already adopted ✅) is likely sufficient for the immediate scope of this PR.

"""Return whether any visualizer path is active for rendering/camera control."""
return bool(self.get_setting("/isaaclab/visualizer/types"))

def can_render_rgb_array(self) -> bool:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Nit: can_render_rgb_array() is defined but never referenced in this PR. If it's intended as public API for downstream code (e.g. gymnasium render_mode="rgb_array" checks), consider adding a brief usage note in the docstring so readers know when to use it vs is_rendering.

Path.home() / ".local" / "share" / "ov" / "data" / "exts" / "v2",
):
if root.exists():
candidate_roots.add(root)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: rglob("pip_prebundle") on broad roots like ~/.local/share/ov/data/exts could be slow on machines with large Omniverse caches (thousands of extensions). This runs during isaaclab install so user-facing latency matters.

Consider adding a maxdepth-style guard (e.g. limit recursion depth) or at minimum logging the discovery time so users can diagnose slow installs:

import time
t0 = time.monotonic()
for root in candidate_roots:
    prebundle_dirs.update(root.rglob("pip_prebundle"))
print_debug(f"Discovered {len(prebundle_dirs)} pip_prebundle dirs in {time.monotonic() - t0:.1f}s")

# In headless mode we keep the visualizer active but skip viewport/window setup.
self._viewport_window = None
self._viewport_api = None
self._controlled_camera_path = "/OmniverseKit_Persp" if self.cfg.enable_visualizer_cam else None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Question: In headless mode with enable_visualizer_cam=True, _controlled_camera_path is hardcoded to "/OmniverseKit_Persp". But the new design creates a dedicated camera prim at visualizer_camera_prim_path (default "/World/Cameras/KitVisualizerCamera") when a viewport API exists. This means headless and GUI modes target different camera prims, which could cause subtle behavioral differences (e.g. a sensor camera at /OmniverseKit_Persp getting its transform overwritten in headless).

Is the headless fallback to /OmniverseKit_Persp intentional? Should it also use visualizer_camera_prim_path for consistency?

create_viewport: bool = False
create_viewport: bool = True
"""Create new viewport with specified name and camera pose."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Nit — breaking change note: create_viewport changed from False to True as default. This is a behavioral change for existing users who instantiate KitVisualizerCfg() without explicit arguments — they'll now get a new viewport window by default. The PR description mentions this is a breaking change, but consider calling it out in the CHANGELOG entry and docstring.

else:
continue
if not _viewer_cfg_value_matches_default(getattr(self, f.name), default_val):
differing.append(f.name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: The __post_init__ deprecation check fires on every ViewerCfg construction, including internal framework code that may use non-default values intentionally (e.g. task configs, test fixtures). The stacklevel=2 may not always point to user code — for configclass-decorated classes, the actual call stack through from_dict() or replace() can be deeper.

Consider:

  1. Using stacklevel=3 or computing it dynamically to ensure the warning points at the user's config file, not framework internals.
  2. Adding a _suppress_deprecation escape hatch for internal usage (e.g. ViewerCfg(_suppress_deprecation=True)).


# launch Kit app
simulation_app = AppLauncher(headless=True, enable_cameras=True).app

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Observation: AppLauncher(headless=True, enable_cameras=True) at module top-level means this file cannot be imported without side effects (Kit app launch). This is standard for Isaac Lab tests, but worth noting that it prevents running individual test functions in isolation via pytest -k without the full app startup. No action needed — just flagging for awareness.

@kellyguo11 kellyguo11 moved this to In progress in Isaac Lab Apr 17, 2026
@kellyguo11
Copy link
Copy Markdown
Contributor

are the changes in #5056 still required with this PR?

@matthewtrepte
Copy link
Copy Markdown
Contributor Author

matthewtrepte commented Apr 17, 2026

are the changes in #5056 still required with this PR?

i think its covered by this PR, and i added Pascal's unit tests here from his PR, and they pass

perhjaps @michaellin6 can confirm

@github-project-automation github-project-automation bot moved this from In progress to Done in Isaac Lab Apr 17, 2026
@matthewtrepte matthewtrepte reopened this Apr 17, 2026
Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot

Summary

This PR makes substantive improvements to the visualizer subsystem: renames camera_position/camera_targeteye/lookat across configs, fixes headless video recording by routing Kit app-loop pumping through PhysicsManager.after_visualizers_render(), adds a ViewerCfg deprecation path, decouples viewport camera control from visualizer broadcasting, and replaces the deleted smoke tests with comprehensive cartpole integration tests that verify non-black frames and motion detection.

The architecture changes are well-motivated — separating Kit-specific rendering from the backend-agnostic SimulationContext, adding render_generation to fix stale-frame dedup, and introducing pumps_app_update() to avoid double-pumping. A few issues are worth addressing before merge.

Design Assessment

Design is sound. The separation of Kit-specific rendering into isaaclab_physx (via kit_viewport_utils.py and after_visualizers_render) keeps SimulationContext backend-agnostic. The pumps_app_update() protocol is a clean way to prevent double-rendering. The ViewerCfg deprecation with __post_init__ field-diffing is thorough. The new integration tests are a significant quality improvement over the deleted smoke tests.

Minor concerns detailed in inline comments.

Test Coverage

  • Bug fix regression tests:test_render_pumps_app_update_without_visualizer and test_render_skips_app_update_when_visualizer_pumps_it directly cover the headless video fix from PR #5056
  • Integration tests: ✅ Comprehensive cartpole integration tests cover Kit, Newton, Rerun, and Viser visualizers with non-black frame and motion-detection assertions
  • Deprecation warning:test_config_update_via_constructor updated to expect DeprecationWarning
  • Paused visualizer behavior:test_update_visualizers_skips_zero_dt_for_paused_app_pumping_visualizer covers the new paused-but-pumping path
  • Gap: No unit test for ViewerCfg.__post_init__ deprecation warning edge cases in isolation (e.g. list vs tuple equivalence in _viewer_cfg_value_matches_default)

CI Status

⚠️ Multiple CI checks failing — appears to be a Docker base image build failure cascading to downstream test suites, not caused by this PR's changes. pre-commit and Build Wheel pass.

Verdict

Minor fixes needed

The approach is correct and well-architected. The inline comments identify a few issues — mostly around error handling robustness and a cross-module dependency concern. None are blocking correctness for the primary use cases.

) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
"""Resolve camera pose from cfg eye/lookat fields."""
del visualizer_name
eye = tuple(float(v) for v in getattr(self.cfg, "eye"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion: getattr(self.cfg, "eye") without a default is equivalent to self.cfg.eye — both raise AttributeError if missing. Direct attribute access is clearer and lets type checkers validate it.

Suggested change
eye = tuple(float(v) for v in getattr(self.cfg, "eye"))
eye = tuple(float(v) for v in self.cfg.eye)

"""Resolve camera pose from cfg eye/lookat fields."""
del visualizer_name
eye = tuple(float(v) for v in getattr(self.cfg, "eye"))
lookat = tuple(float(v) for v in getattr(self.cfg, "lookat"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above:

Suggested change
lookat = tuple(float(v) for v in getattr(self.cfg, "lookat"))
lookat = tuple(float(v) for v in self.cfg.lookat)

import isaacsim.core.utils.viewports as isaacsim_viewports

isaacsim_viewports.set_camera_view(eye=list(eye), target=list(target), camera_prim_path=str(camera_prim_path))
except Exception as exc:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: This bare except Exception silently downgrades all errors to DEBUG level. If the camera prim path is wrong or the viewport state is invalid, the user gets no visible feedback — the camera just silently stays in the wrong position.

Consider splitting expected vs unexpected failures:

Suggested change
except Exception as exc:
except (ImportError, ModuleNotFoundError) as exc:
logger.debug("[kit_viewport] Renderer camera update skipped (no Kit): %s", exc)
except Exception as exc:
logger.warning("[kit_viewport] Renderer camera update failed: %s", exc)

try:
ensure_isaac_rtx_render_update()
except Exception as exc:
logger.debug("[isaac_rtx] Skipping Kit app-loop pump in render(): %s", exc)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: Same concern — except Exception with logger.debug in the --video headless recording path means Fabric sync errors, USD context corruption, etc. would silently produce stale/black frames with no user-visible indication.

Since the user explicitly requested --video, a failure here directly impacts their output:

Suggested change
logger.debug("[isaac_rtx] Skipping Kit app-loop pump in render(): %s", exc)
logger.warning("[isaac_rtx] Kit app-loop pump failed in render() — video frames may be stale: %s", exc)

self._env.sim.set_camera_view(eye=cam_eye, target=cam_target)
# set the renderer viewport camera view (does not broadcast to visualizers)
# Kit-specific helper lives in isaaclab_physx
from isaaclab_physx.renderers.kit_viewport_utils import set_kit_renderer_camera_view
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: This introduces a hard import from isaaclab (core) → isaaclab_physx (backend-specific package). ViewportCameraController lives in the core package but now depends on isaaclab_physx.renderers.kit_viewport_utils at call time.

This is mitigated by the has_kit() guard at the env level (the controller is only created when Kit is present), but the import will raise ModuleNotFoundError in a pure-Newton deployment that somehow reaches this code path. The previous self._env.sim.set_camera_view() was backend-agnostic.

Consider wrapping with a fallback:

try:
    from isaaclab_physx.renderers.kit_viewport_utils import set_kit_renderer_camera_view
    set_kit_renderer_camera_view(eye=cam_eye, target=cam_target, camera_prim_path=self.cfg.cam_prim_path)
except (ImportError, ModuleNotFoundError):
    pass  # Kit-only; Newton-only deployments skip viewport camera

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this whole class applicable to other visualizer/sensors? I think we may want to look at decoupling this class between the controller and viewport logic so that it can be applied to other visualizers and avoids backend-specific references.

self._viewport_window = None
self._viewport_api = None
self._controlled_camera_path = "/OmniverseKit_Persp" if self.cfg.enable_visualizer_cam else None
if not self.cfg.enable_visualizer_cam:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion: In the headless path, _controlled_camera_path is set to /OmniverseKit_Persp but _create_and_assign_camera is not called (no dedicated camera prim is created). Then _apply_cfg_camera_pose_if_configured() modifies the default perspective camera directly.

If another component (e.g., a render-product annotator or sensor camera) shares /OmniverseKit_Persp, they'd have conflicting camera state. This seems intentional for headless simplicity — worth a brief comment explaining why headless skips dedicated camera prim creation (e.g., "no viewport to bind a dedicated camera to in headless mode").

"""Viewport name to use when :attr:`create_viewport` is True."""

create_viewport: bool = False
create_viewport: bool = True
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion: The default changed from False to True. This is a breaking change for anyone who relied on the old default — the Kit visualizer previously used the active viewport, now it creates a new one. The PR description mentions breaking changes — this should be called out in the CHANGELOG so users know to set create_viewport=False if they want the previous behavior.

continue
if viz.is_rendering_paused():
# Keep non-Kit visualizer event loops responsive while rendering is paused.
if not viz.pumps_app_update():
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion: Good design — calling viz.step(0.0) for non-app-pumping visualizers keeps their GL event loops (keyboard/mouse for Newton/Rerun/Viser resume buttons) responsive while rendering is paused. Kit is correctly skipped since its step() calls app.update() which shouldn't run during pause.

Consider expanding the comment slightly to explain why this matters (e.g., "Newton/Rerun/Viser need step(0.0) to process user input like the Resume button").

@matthewtrepte matthewtrepte requested a review from Toni-SM as a code owner April 17, 2026 23:24
@kellyguo11 kellyguo11 moved this from Done to In review in Isaac Lab Apr 17, 2026
@kellyguo11
Copy link
Copy Markdown
Contributor

can you check the test failures? visualizer tests seem to be failing, not sure if other tests are related to the change.

@matthewtrepte
Copy link
Copy Markdown
Contributor Author

matthewtrepte commented Apr 21, 2026

can you check the test failures? visualizer tests seem to be failing, not sure if other tests are related to the change.

Its the frozen visualizer test that's been added which is failing on the kit bug.

=================================== FAILURES ===================================
_____________ test_kit_visualizer_non_black_viewport_frame[physx] ______________
source/isaaclab_visualizers/test/test_visualizer_cartpole_integration.py:470: in test_kit_visualizer_non_black_viewport_frame
    _run_kit_viewport_frame_motion_test(env, kit_visualizers[0], physics_kind=backend_kind)
source/isaaclab_visualizers/test/test_visualizer_cartpole_integration.py:408: in _run_kit_viewport_frame_motion_test
    _assert_early_and_late_motion_frames_differ(frames)
source/isaaclab_visualizers/test/test_visualizer_cartpole_integration.py:316: in _assert_early_and_late_motion_frames_differ
    assert n_diff >= min_differing_pixels, (
E   AssertionError: Viewport captures #2 and #60 have too few strongly differing pixels (30 < 100; threshold per channel=50 in 0–255 space). Possible frozen or stale robot visualization.
E   assert 30 >= 100
--------------------------

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation isaac-lab Related to Isaac Lab team isaac-mimic Related to Isaac Mimic team

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

2 participants